package org.xx.armory.service.security.impl;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xx.armory.service.ServiceContext;
import org.xx.armory.service.TempFileObject;
import org.xx.armory.service.security.CaptchaImageProvider;

import java.awt.*;
import java.awt.font.FontRenderContext;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.awt.image.RenderedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Random;

import static java.awt.RenderingHints.KEY_TEXT_ANTIALIASING;
import static java.awt.RenderingHints.VALUE_TEXT_ANTIALIAS_ON;
import static javax.imageio.ImageIO.write;
import static org.xx.armory.commons.Validators.greaterThan;
import static org.xx.armory.commons.Validators.notBlank;
import static org.xx.armory.commons.Validators.notNull;

/**
 * @author Haart
 */
public final class DefaultCaptchaImageProvider
        implements CaptchaImageProvider {
    private static final int DEFAULT_CODE_LENGTH = 4;
    private static final int DEFAULT_IMAGE_WIDTH = 60;
    private static final int DEFAULT_IMAGE_HEIGHT = 24;
    private static final AffineTransform IDENTITY_TRANS = new AffineTransform();
    private static final FontRenderContext PRIMARY_CONTEXT = new FontRenderContext(IDENTITY_TRANS, true, false);
    private static final Font PRIMARY_FONT = new Font(Font.SERIF, Font.BOLD, 48);
    /**
     * <strong>Except 'l', 'L', 'O', 'o', '0' etc... </strong>
     */
    private static final char[] CODE_SET = {'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J',
            'K', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W',
            'X', 'Y', 'Z', '2', '3', '4', '5', '6', '7', '8', '9'};
    private final Logger logger = LoggerFactory.getLogger(DefaultCaptchaImageProvider.class);

    /**
     *
     */
    public DefaultCaptchaImageProvider() {
    }

    /**
     * 生成随机码。
     *
     * @param codeLength
     *         随机码的长度。
     * @return 生成的随机码, 只包含预设的字符。
     * @throws IllegalArgumentException
     *         如果参数{@code codeLength}小于或者等于{@code 0}。
     */
    public static String randomCode(final int codeLength) {
        greaterThan(codeLength, "codeLength", 0);

        final StringBuilder sb = new StringBuilder();
        final Random rnd = new Random();

        for (int i = 0; i < codeLength; ++i) {
            sb.append(CODE_SET[rnd.nextInt(CODE_SET.length)]);
        }

        return sb.toString();
    }

    /**
     * 生成验证码图片。
     *
     * @param captchaCode
     *         随机验证码。
     * @param width
     *         图片宽度(单位是像素)。
     * @param height
     *         图片高度(单位是像素)。
     * @return 表示图片内容的字节数组。
     * @throws IOException
     *         如果将图片保存到字节数组时出错。
     * @throws IllegalArgumentException
     *         如果参数{@code captchaCode}是{@code null}或者只包含空白字符, 或者参数{@code width}小于等于{@code 10}, 或者参数{@code height}
     *         小于等于{@code 10}。
     */
    public static byte[] drawImage(
            final String captchaCode,
            final int width,
            final int height
    )
            throws IOException {
        notBlank(captchaCode, "captchaCode");
        greaterThan(width, "width", 10);
        greaterThan(height, "height", 10);

        final Random rnd = new Random();
        final double lineY1 = rnd.nextDouble() * .4 + .3;

        // Prepare regular image
        final Rectangle2D rect1 = PRIMARY_FONT.getStringBounds(captchaCode, PRIMARY_CONTEXT);
        final BufferedImage imageCode = new BufferedImage(
                (int) rect1.getWidth(), (int) rect1.getHeight(), BufferedImage.TYPE_INT_RGB);
        final Graphics2D gCode = imageCode.createGraphics();
        gCode.setColor(Color.WHITE);
        gCode.fillRect(0, 0, (int) rect1.getWidth(), (int) rect1.getHeight());
        gCode.setColor(Color.BLUE);
        // Draw code text
        gCode.setFont(PRIMARY_FONT);
        gCode.setRenderingHint(KEY_TEXT_ANTIALIASING, VALUE_TEXT_ANTIALIAS_ON);
        final double baseY = -rect1.getY();
        gCode.drawString(captchaCode, 0, (int) baseY);
        // Draw disturb line
        final int x2 = (int) rect1.getWidth();
        final int y1 = (int) (rect1.getHeight() * lineY1);
        final int y2 = (int) (rect1.getHeight() * (1 - lineY1));
        gCode.drawLine(0, y1, x2, y2);
        gCode.drawLine(0, y1 - 1, x2, y2 - 1);
        gCode.drawLine(0, y1 + 1, x2, y2 + 1);
        gCode.dispose();

        // Map to destination image
        final BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
        final Graphics2D g = image.createGraphics();
        g.setColor(new Color(0xFF, 0xFF, 0xFF, 0x0));
        g.fillRect(0, 0, width, height);
        g.drawRenderedImage(imageCode, transform(rect1.getWidth(), rect1.getHeight(), width, height));
        g.dispose();

        try (final ByteArrayOutputStream output = new ByteArrayOutputStream()) {
            write(image, "PNG", output);
            return output.toByteArray();
        }
    }

    /**
     * 生成一个线性的渐变矩阵。
     * <p>该矩阵用于以扭曲的方式绘制图形</p>
     *
     * @param w1
     *         原始图形宽度。
     * @param h1
     *         原始图形高度。
     * @param w2
     *         扭曲后的目标宽度。
     * @param h2
     *         扭曲后的目标高度。
     * @return 渐变矩阵。
     * @see Graphics2D#drawRenderedImage(RenderedImage, AffineTransform)
     */
    private static AffineTransform transform(
            double w1,
            double h1,
            double w2,
            double h2
    ) {
        final double SHEARING_K = 0.1D; /* It should between 0D (exclusive) and 1D (exclusive) */

        final Random rnd = new Random();
        final boolean shift = (rnd.nextInt() & 0x01) == 0;
        final double shearingX = SHEARING_K * w2 / h2;
        return new AffineTransform(w2 * (1D - SHEARING_K) / w1, 0D, shift ? shearingX : -shearingX,
                                   h2 / h1, shift ? 0D : w2 * SHEARING_K, 0D);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public final TempFileObject execute(ServiceContext ctx) {
        notNull(ctx, "ctx");

        // 生成随机验证码。
        final String captchaCode = randomCode(DEFAULT_CODE_LENGTH);

        // 将验证码放进令牌。
        ctx.getToken().putString(CAPTCHA_KEY, captchaCode);

        // 生成图片。
        try {
            return new TempFileObject("captcha-image.png", drawImage(captchaCode, DEFAULT_IMAGE_WIDTH, DEFAULT_IMAGE_HEIGHT));
        } catch (IOException ex) {
            logger.error("cannot draw captcha image", ex);
            return TempFileObject.NONE;
        }
    }
}
